/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseType, FieldType, SRDiscriminator} from './SRScene';
import { classFactory, enumHelper, id, T } from './nameUtils';

export type Type = FieldType & {
    optional: boolean;
    discriminator?: SRDiscriminator;
}

export type TemplateSet = {
    typeName: (t: Type) => string;
    convertType: (t: Type) => string;
    ctor: (t: Type) => string | null;
    copy: (t: Type) => (value: string, dest?: string) => string;
    toJson: (t: Type) => (value: string, dest?: string) => string;
}

function ts(typeName: TemplateSet['typeName'], convertType: TemplateSet['convertType'],
    ctor: TemplateSet['ctor'], copy: TemplateSet['copy'], toJson: TemplateSet['toJson']
): TemplateSet {
    return { typeName, convertType, ctor, copy, toJson };
}

export function template<T extends keyof TemplateSet>(type: FieldType, transformation: T) {
    return fieldTemplates[type.category][transformation];
}

const bigIntType = () => 'bigint';
const booleanType = () => 'boolean';
const numberType = () => 'number';
const stringType = () => 'string';
const objectType = () => 'object';
const sendableType = () => 'lang.ISendable';
const enumType = (t: FieldType) => t.name;
const classType = (t: FieldType) => t.name;
const unionType = (t: FieldType) => {
    return t.genericTypes.map(type=>{
        const innerType = {...t, ...type, optional:false};
        return `${template(type,"convertType")(innerType)}`; 
    }).join("|")
};
const arrayType = (t: FieldType) => `Array<${t.genericTypes[0].name}>`;
const mapType = (t: FieldType) => `Map<${t.genericTypes[0].name}, ${t.genericTypes[1].name}>`;
const setType = (t: FieldType) => `Set<${t.genericTypes[0].name}>`;

const sendableArray = (t: FieldType) => {
    const innerType = {...t, ...t.genericTypes[0], optional:false};
    return `collections.Array<${template(t.genericTypes[0],"convertType")(innerType)}>`;
}

const sendableMap = (t: FieldType) => {
    const keyType = template(t.genericTypes[0],"convertType")({...t, ...t.genericTypes[0], optional:false});
    const valueType = template(t.genericTypes[1],"convertType")({...t, ...t.genericTypes[1], optional:false});
    return `collections.Map<${keyType}, ${valueType}>`;
}
    
const sendableSet = (t: FieldType) => {
    const innerType = {...t, ...t.genericTypes[0], optional:false};
    return `collections.Set<${template(t.genericTypes[0],"convertType")(innerType)}>`;
}

const noCtor = () => null;

const bigIntCopy = () => (value: string, dest?: string) =>
        dest ? `${dest} = BigInt(${value})`: `BigInt(${value} as (string | number | bigint))`;
const booleanCopy = () => (value: string, dest?: string) =>
        dest ? `${dest} = Boolean(${value})`: `Boolean(${value})`;
const numberCopy = () => (value: string, dest?: string) =>
        dest ? `${dest} = Number(${value})`: `Number(${value})`;
const stringCopy = () => (value: string, dest?: string) =>
        dest ? `${dest} = String(${value})`: `String(${value})`;
const objectCopy = () => (value: string, dest?: string) =>
        dest ? `${dest} = ${value} as ${sendableType()}`: `${value} as ${sendableType()}`;
const unionCopy = () => (value: string, dest?: string) => `${dest} = ${value}`
const enumCopy = (t: FieldType) => (value: string, dest?: string) =>
        dest ? `${dest} = ${enumHelper(t.name)}.toEnum(${value})`: `${enumHelper(t.name)}.toEnum(${value})`;

const classCopy = (t: Type) => (value: string, dest?: string) => {
    if (!t.discriminator?.getSubTypes().length) {
        return dest ? `${dest} = ${classFactory(t.name)}.instance.fromJson(${value})` : 
            `${classFactory(t.name)}.instance.fromJson(${value})`;
    } else {
        // Handle class-type field with inheritance information
        return `let field: object = ${value};
${T}${T}${T}if (!field['__type']) {
${T}${T}${T}${T}throw new Error('No __type field in jsonObj: ' + JSON.stringify(jsonObj));
${T}${T}${T}}
${T}${T}${T}switch (field['__type']) {
${T}${T}${T}${T}${t.discriminator.getSubTypes().map((type, index, types) => 
            `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}${dest} = ${classFactory(type.value.getName())}.instance.fromJson(field);
${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}}`).join('\n' + T.repeat(4))}
${T}${T}${T}}`;
    }
}
const arrayCopy = (t: Type) => (value: string, dest?: string) => {
    const innerType = { ...t, ...t.genericTypes[0], optional: false };
    const innerConvertType = template(innerType, 'convertType')(innerType);
    const innerCopy = template(innerType, 'copy')(innerType);
    const initExpr = t.optional? `${dest} ??= new collections.Array<${innerConvertType}>();\n` : '';
    if (!innerType.discriminator?.getSubTypes().length) {
        return `${initExpr}
${T}${T}${T}for (let item of ((${value}) as Object[])) {
${T}${T}${T}${T}${dest}.push(${innerCopy('item')});
${T}${T}${T}}`;
    } else {
        // Handle element of class-type with inheritance information in array
        return `${initExpr}
${T}${T}${T}for (let item of ((${value}) as object[])) {
${T}${T}${T}${T}if (!item['__type']) {
${T}${T}${T}${T}${T}throw new Error('No __type field in item: ' + JSON.stringify(item));
${T}${T}${T}${T}}
${T}${T}${T}${T}switch (item['__type']) {
${T}${T}${T}${T}${T}${innerType.discriminator.getSubTypes().map((type, index, types) => 
                `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}${T}${dest}.push(${classFactory(type.value.getName())}.instance.fromJson(item));
${T}${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}${T}}`).join('\n' + T.repeat(5))}
${T}${T}${T}${T}}
${T}${T}${T}}`;
    }
}
const mapCopy = (t: Type) => (value: string, dest?: string) => {
    const keyType = { ...t, ...t.genericTypes[0], optional: false };
    const keyConvertType = template(keyType, 'convertType')(keyType);
    const valueType = { ...t, ...t.genericTypes[1], optional: false };
    const valueConvertType = template(valueType, 'convertType')(valueType);
    const valueCopy = template(valueType, 'copy')(valueType);
    const initExpr = t.optional? `${dest} ??= new collections.Map<${keyConvertType}, ${valueConvertType}>();` : '';
    if (!valueType.discriminator?.getSubTypes().length) {
        return `${initExpr}
${T}${T}${T}const map: collections.Map<${keyConvertType}, lang.ISendable> = ${value};
${T}${T}${T}for (let entry of map) {
${T}${T}${T}${T}${dest}.set(entry[0], ${valueCopy('entry[1]')});
${T}${T}${T}}`;
    } else {
        // Handle value of class-type with inheritance information in map
        return `${initExpr}
${T}${T}${T}const map: collections.Map<${keyConvertType}, ${valueConvertType}> = ${value};
${T}${T}${T}for (let entry of map) {
${T}${T}${T}${T}if (!entry[1].__type) {
${T}${T}${T}${T}${T}throw new Error('No __type field in entry[1]: ' + JSON.stringify(entry[1]));
${T}${T}${T}${T}}
${T}${T}${T}${T}switch (entry[1].__type) {
${T}${T}${T}${T}${T}${valueType.discriminator.getSubTypes().map((type, index, types) => 
                `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}${T}${dest}.set(entry[0], ${classFactory(type.value.getName())}.instance.fromJson(entry[1]));
${T}${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}${T}}`).join('\n' + T.repeat(5))}
${T}${T}${T}${T}}
${T}${T}${T}}`;
    }
}

const setCopy = (t: Type) => (value: string, dest?: string) => {
    const innerType = { ...t, ...t.genericTypes[0], optional: false };
    const innerConvertType = template(innerType, 'convertType')(innerType);
    const innerCopy = template(innerType, 'copy')(innerType);
    const initExpr = t.optional? `${dest} ??= new collections.Set<${innerConvertType}>();` : '';
    if (!innerType.discriminator?.getSubTypes().length) {
        return `${initExpr}
${T}${T}${T}for (let item of ((${value}) as Object[])) {
${T}${T}${T}${T}${dest}.add(${innerCopy('item')});
${T}${T}${T}}`;
    } else {
        // Handle element of class-type with inheritance information in set
        return `${initExpr}
${T}${T}${T}for (let item of ((${value}) as object[])) {
${T}${T}${T}${T}if (!item['__type']) {
${T}${T}${T}${T}${T}throw new Error('No __type field in item: ' + JSON.stringify(item));
${T}${T}${T}${T}}
${T}${T}${T}${T}switch (item['__type']) {
${T}${T}${T}${T}${T}${innerType.discriminator.getSubTypes().map((type, index, types) => 
                `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}${T}${dest}.add(${classFactory(type.value.getName())}.instance.fromJson(item));
${T}${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}${T}}`).join('\n' + T.repeat(5))}
${T}${T}${T}${T}}
${T}${T}${T}}`;
    }
}


const keep = (t: FieldType) => (value: string, dest?: string) => `${dest} = this.${id(value)};`;
const arrayToJson = (t: Type) => {
    const innerToJson = template(t.genericTypes[0], 'toJson');
    const innerType = {...t, ...t.genericTypes[0], optional:false};
    if (innerToJson === keep) {
        return (value: string, dest?: string) => `${dest} = Array.from(this.${value})`;
    } else if (!innerType.discriminator?.getSubTypes().length) {
        return (value: string, dest?: string) => `${dest} = Array.from(this.${value}).map(x => ${innerToJson(innerType)('x')})`;
    } else {
        // Handle element of class-type with inheritance information in array
        return (value: string, dest?: string) => 
        `${dest} = Array.from(this.${value}.map(x => {
${T}${T}${T}if (!x.__type) {
${T}${T}${T}${T}throw new Error('__type field is empty, do not know to serialize. ');
${T}${T}${T}}
${T}${T}${T}switch (x.__type) {
${T}${T}${T}${T}${innerType.discriminator?.getSubTypes().map(type => 
                `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}return ${classFactory(type.value.getName())}.instance.toJson(x as ${type.value.getName()});
${T}${T}${T}${T}}`).join('\n' + T.repeat(4))}
${T}${T}${T}}
${T}${T}${T}return x;
${T}${T}}));`;
    }
};
const mapToJson = (t: Type) => {
    const innerToJson = template(t.genericTypes[1], 'toJson');
    const valueType = {...t, ...t.genericTypes[1], optional:false};
    if (innerToJson === keep) {
        return (value: string, dest?: string) => `${dest} = new Map(this.${value}.entries())`;
    } else if (!valueType.discriminator?.getSubTypes().length) {
        return (value: string, dest?: string) => `
${T}${T}const ${value} = new Map<string, object>();
${T}${T}for (let entry of this.${value}.entries()) {
${T}${T}${T}${value}.set(entry[0], ${innerToJson(valueType)('entry[1]')});
${T}${T}}
${T}${T}${dest} = ${value};
`;
    } else {
        // Handle value of class-type with inheritance information in map
        return (value: string, dest?: string) => 
        `if (this.${value}) {
${T}${T}${T}const ${value} = new Map<string, object>();
${T}${T}${T}for (let entry of this.${value}.entries()) {
${T}${T}${T}${T}if (!entry[1].__type) {
${T}${T}${T}${T}${T}throw new Error('__type field is empty, do not know to serialize. ');
${T}${T}${T}${T}}
${T}${T}${T}${T}switch (entry[1].__type) {
${T}${T}${T}${T}${T}${valueType.discriminator?.getSubTypes().map((type, index, types) => 
                    `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}${T}${value}.set(entry[0], ${classFactory(type.value.getName())}.instance.toJson(entry[1] as ${type.value.getName()}));
${T}${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}${T}}`).join('\n' + T.repeat(5))}
${T}${T}${T}${T}}
${T}${T}${T}}
${T}${T}${T}${dest} = ${value};
${T}${T}}`;
    }
};
const setToJson = (t: Type) => {
    const innerToJson = template(t.genericTypes[0], 'toJson');
    const innerType = {...t, ...t.genericTypes[0], optional:false};
    if (innerToJson === keep) {
        return (value: string, dest?: string) => `${dest} = Array.from(this.${value}.keys())`;
    } else if (!innerType.discriminator?.getSubTypes().length) {
        return (value: string, dest?: string) => `
${T}${T}const ${value} = new Set<object>();
${T}${T}for (let item of this.${value}.keys()) {
${T}${T}${T}${value}.add(${innerToJson(innerType)('(item)')});
${T}${T}}
${T}${T}${dest} = ${value};
`;
    } else {
        // Handle element of class-type with inheritance information in set
        return (value: string, dest?: string) => `if (this.${value}) {
${T}${T}${T}const ${value} = new Set<object>();
${T}${T}${T}for (let item of this.${value}.keys()) {
${T}${T}${T}${T}if (!item.__type) {
${T}${T}${T}${T}${T}throw new Error('__type field is empty, do not know to serialize. ');
${T}${T}${T}${T}}
${T}${T}${T}${T}switch (item.__type) {
${T}${T}${T}${T}${T}${innerType.discriminator?.getSubTypes().map((type, index, types) => 
                    `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}${T}${value}.add(${classFactory(type.value.getName())}.instance.toJson(item as ${type.value.getName()}));
${T}${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}${T}}`).join('\n' + T.repeat(5))}
${T}${T}${T}${T}}
${T}${T}${T}}
${T}${T}${T}${dest} = ${value};
${T}${T}}`;
    }
};
const classToJson = (t: Type) => (value: string, dest?: string) => {
    if (!t.discriminator?.getSubTypes().length) {
        return dest? `${dest} = ${classFactory(t.name)}.instance.toJson(this.${value});`: `${classFactory(t.name)}.instance.toJson(${value})`;
    } else {
        // Handle class with inheritance information
        return `if (!this.${value}.__type) {
${T}${T}${T}${T}throw new Error('__type field is empty, do not know to serialize. ');
${T}${T}${T}}
${T}${T}${T}switch (this.${value}.__type) {
${T}${T}${T}${T}${t.discriminator?.getSubTypes().map((type, index, types) => 
                `case '${type.value.getName()}': {
${T}${T}${T}${T}${T}ans['${value}'] = ${classFactory(type.value.getName())}.instance.toJson(this.${value} as ${type.value.getName()});
${T}${T}${T}${T}${T}${index != types.length - 1? 'break;\n': ''}
${T}${T}${T}${T}}`).join('\n' + T.repeat(4))}
${T}${T}${T}}`;
    } 
}
        

const fieldTemplates: Record<BaseType, TemplateSet> = {
    //                               typeName      convertType        ctor         copy       toJson
    [BaseType.BIGINIT]:           ts(bigIntType , bigIntType   , noCtor       , bigIntCopy , keep),
    [BaseType.BOOLEAN]:           ts(booleanType, booleanType  , noCtor       , booleanCopy, keep),
    [BaseType.NUMBER]:            ts(numberType , numberType   , noCtor       , numberCopy , keep),
    [BaseType.STRING]:            ts(stringType , stringType   , noCtor       , stringCopy , keep),
    [BaseType.OBJECT]:            ts(objectType , sendableType , noCtor       , objectCopy , keep),
    [BaseType.ARRAY]:             ts(arrayType  , sendableArray, sendableArray, arrayCopy  , arrayToJson),
    [BaseType.MAP]:               ts(mapType    , sendableMap  , sendableMap  , mapCopy    , mapToJson),
    [BaseType.SET]:               ts(setType    , sendableSet  , sendableSet  , setCopy    , setToJson),
    [BaseType.ENUM]:              ts(enumType   , enumType     , noCtor       , enumCopy   , keep),
    [BaseType.OTHERS]:            ts(classType  , classType    , noCtor       , classCopy  , classToJson),
    [BaseType.UNION]:             ts(unionType  , unionType    , noCtor       , unionCopy  , keep),
    [BaseType.CLASS]:             ts(classType  , classType    , noCtor       , classCopy  , classToJson),
    [BaseType.COLLECTIONS_ARRAY]: ts(arrayType  , sendableArray, sendableArray, arrayCopy  , arrayToJson),
    [BaseType.COLLECTIONS_MAP]:   ts(mapType    , sendableMap  , sendableMap  , mapCopy    , mapToJson),
    [BaseType.COLLECTIONS_SET]:   ts(setType    , sendableSet  , sendableSet  , setCopy    , setToJson)

}